package es.udima.geo.geoserver.business;

import java.util.Calendar;
import java.util.List;

import es.udima.geo.geoserver.entities.GpsData;
import es.udima.geo.geoserver.entities.GpsDataFilter;
import es.udima.geo.geoserver.entities.GpsDataRetrieveResult;
import es.udima.geo.geoserver.persistence.GpsDataDAO;

/**
 * <p>Clase controladora de los datos GPS.</p> 
 * <p>Es la encargada de realizar las funciones de negocio relativas a los datos GPS.</p> 
 * 
 * @author david
 *
 */
public class GpsDataController {
    
    /**
     * Constructor
     */
    public GpsDataController() {
    }

    /**
     * <p>Recupera los datos GPS almacenados que concuerden con las condiciones del filtro pasado como parámetro.</p>
     * <p>El filtro debe incluir como mínimo la condición del id de usuario del que se quiere recuperar los datos.</p>
     * <p>Si el filtro no incluye fechas se recuperarán los datos gps del último día que se hayan almacenado datos de ese usuario.</p>
     * 
     * @param filter Filtro con las condiciones para la recuperación de datos.
     * @return Resultado de la consulta.
     */
    public GpsDataRetrieveResult retrieveGpsData(GpsDataFilter filter) {

	GpsDataDAO dao = new GpsDataDAO();

	GpsDataFilter resFilter = new GpsDataFilter();
	resFilter.setUserId(filter.getUserId());

	// si se ha activado el atributo userId pero ninguna de las fechas se
	// recuperan los datos gps del ultimo dia disponible del usuario
	if (!filter.getUserId().isEmpty() && filter.getInitDate() == null && filter.getEndDate() == null) {

	    // establecemos como fecha de inicio y fin la ultima disponible del
	    // usuario
	    Calendar initDate = dao.retrieveLastDayGpsData(resFilter.getUserId());
	    if (initDate != null) {
		resFilter.setInitDate(initDate);
		resFilter.setEndDate(initDate);
	    }
	} else {
	    resFilter.setInitDate(filter.getInitDate());
	    resFilter.setEndDate(filter.getEndDate());
	}

	List<GpsData> gpsData = dao.retrieveGpsData(resFilter);

	if (gpsData != null) {
	    calculateSpeedData(gpsData);
	}

	GpsDataRetrieveResult res = new GpsDataRetrieveResult();
	res.setFilter(resFilter);
	res.setGpsData(gpsData);

	return res;
    }

    /**
     * <p>Graba en base de datos la lista de datos gps pasada como parámetro.</p>
     * 
     * @param gspData Lista de datos gps que se desea almacenar.
     */
    public void storeGpsData(List<GpsData> gpsData) {
	GpsDataDAO dao = new GpsDataDAO();
	dao.storeGpsData(gpsData);
    }

    /*------------------------------------------------------------------------------*/

    /**
     * <p>Calcula las velocidades y distancia entre las coordenadas de los datos gps de la lista pasada como parámetro.</p>
     * <p>El cálculo lo hace entre el punto n_i, n_i+1 según el orden en que se han pasado en la lista.</p>
     * <p>Para obtener un resultado correcto la lista deberá estar ordenada de menor a mayor tiempo de captura.</p>
     * 
     * @param gpsData Lista de datos gps sobre la que se calcularán las distancias y velocidades.
     */
    private void calculateSpeedData(List<GpsData> gpsData) {
	GpsData gdPrev = null;
	for (GpsData gd : gpsData) {
	    if (gdPrev == null) {
		gdPrev = gd;
	    } else {
		calculateSpeedData(gdPrev, gd);
		gdPrev = gd;
	    }
	}
    }

    /**
     * <p>Calcula la velocidad y distancia entre las coordenadas de los datos gps pasados como parámetro.</p>
     * <p>Para obtener un resultado el dato gd1 deberá tener un tiempo de captura menor al de gd2.</p>
     * 
     * @param gd1 Datos gps que incluyen la primera coordenada.
     * @param gd2 Datos gps que incluyen la segunda coordenada.
     */
    private void calculateSpeedData(GpsData gd1, GpsData gd2) {

	double distance = calculateDistance(gd1.getLat(), gd1.getLon(), gd1.getLat(), gd2.getLon()) * 1000;
	double time = gd2.getFetchTime().getTimeInMillis() - gd1.getFetchTime().getTimeInMillis();
	double speed = (distance / 1000) / (time / (3600 * 1000));

	gd2.setDistanceFromPrevious(distance);
	gd2.setLapseTimeFromPrevious(time);
	gd2.setSpeedFromPrevious(speed);
    }

    /**
     * <p>Calcula la distancia en kilómetros entre las coordenadas gps pasadas como parámetro.</p>
     * <p>Distancia = 6371 * ACOS(COS(Lat1) * COS(Lat2) * COS(Long2 - Long1) + SIN(Lat1) * SIN(Lat2))</p>
     * <p>donde:</p>
     * <p>Lat1=(LATITUD1*3.141592654)/180 Lat2=(LATITUD2*3.141592654)/180</p>
     * <p>Long1=(LONGITUD1*3.141592654)/180 Long2=(LONGITUD2*3.141592654)/180</p>
     * 
     * <p>Fórmula extraida de las siguientes fuentes:</p> 
     * <p><a href="http://www.xoneisp.com/foro/showthread.php?tid=294" target="_blank">http://www.xoneisp.com/foro/showthread.php?tid=294</a></p>
     * <p><a href="http://latinometro.blogspot.com.es/2007/10/distancia-y-rumbo.html" target="_blank">http://latinometro.blogspot.com.es/2007/10/distancia-y-rumbo.html</a></p>
     * 
     * @param lat1 Latitud de la primera coordenada.
     * @param lon1 Longitud de la primera coordenada.
     * @param lat2 Latitud de la segunda coordenada.
     * @param lon2 Longitud de la segunda coordenada.
     * 
     * @return Distancia en kilómetros entre los puntos indicados por la coordenadas. 
     */
    private double calculateDistance(double lat1, double lon1, double lat2, double lon2) {
	double la1 = lat1 * Math.PI / 180;
	double lo1 = lon1 * Math.PI / 180;
	double la2 = lat2 * Math.PI / 180;
	double lo2 = lon2 * Math.PI / 180;

	double distance = 6371 * Math.acos(Math.cos(la1) * Math.cos(la2) * Math.cos(lo2 - lo1) + Math.sin(la1) * Math.sin(la2));

	return distance;
    }
}
